spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1/**2 * Same-origin proxy for the admin console: /admin/api/<path> → API /api/admin/<path> (or /api/v1/<path> when the3 * first segment is `v1`). Adds `x-dci-admin-token` from the httpOnly cookie server-side, forwards method / query /4 * body, streams the upstream body back (raw document bodies included). Mutations require a same-origin5 * `Sec-Fetch-Site` (or a matching `Origin`) — CSRF guard on top of the SameSite cookie.6 */7import { NextResponse, type NextRequest } from "next/server";8import { ADMIN_COOKIE, adminApiBase } from "@/lib/admin-api";910export const dynamic = "force-dynamic";11export const maxDuration = 300;1213const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._:@-]*$/;14const UPSTREAM_TIMEOUT_MS = 290_000;15const PASS_RESPONSE_HEADERS = ["content-type", "content-disposition", "content-length", "x-storage-key", "x-truncated", "x-cache", "etag", "x-ratelimit-limit", "x-ratelimit-remaining"];1617function json(status: number, body: Record<string, unknown>): NextResponse {18 const res = NextResponse.json(body, { status });19 res.headers.set("cache-control", "no-store");20 res.headers.set("x-robots-tag", "noindex, nofollow");21 return res;22}2324function sameOrigin(req: NextRequest): boolean {25 const sfs = req.headers.get("sec-fetch-site");26 if (sfs) return sfs === "same-origin";27 const origin = req.headers.get("origin");28 if (!origin) return false;29 try {30 const o = new URL(origin);31 const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? req.nextUrl.host;32 return o.host === host;33 } catch {34 return false;35 }36}3738async function proxy(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }): Promise<Response> {39 const token = req.cookies.get(ADMIN_COOKIE)?.value;40 if (!token) return json(401, { error: "unauthorized", statusCode: 401 });4142 const { path } = await ctx.params;43 if (!Array.isArray(path) || path.length === 0) return json(404, { error: "not found", statusCode: 404 });44 for (const seg of path) if (!SEGMENT.test(seg)) return json(400, { error: "invalid path", statusCode: 400 });4546 const method = req.method.toUpperCase();47 const mutation = method !== "GET" && method !== "HEAD";48 if (mutation && !sameOrigin(req)) return json(403, { error: "cross-site request blocked", statusCode: 403 });4950 const [first, ...rest] = path;51 const upstreamPath = first === "v1" ? `/api/v1/${rest.map(encodeURIComponent).join("/")}` : `/api/admin/${path.map(encodeURIComponent).join("/")}`;52 const url = `${adminApiBase()}${upstreamPath}${req.nextUrl.search}`;5354 const headers = new Headers();55 headers.set("x-dci-admin-token", token);56 headers.set("accept", req.headers.get("accept") ?? "application/json");57 const ct = req.headers.get("content-type");58 if (ct) headers.set("content-type", ct);59 const inm = req.headers.get("if-none-match");60 if (inm) headers.set("if-none-match", inm);61 headers.set("x-forwarded-for", req.headers.get("x-forwarded-for") ?? "127.0.0.1");62 headers.set("user-agent", "dci-web-admin-proxy/1.0");6364 const init: RequestInit & { duplex?: "half" } = {65 method,66 headers,67 redirect: "manual",68 cache: "no-store",69 signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),70 };71 if (mutation && req.body) {72 init.body = req.body;73 init.duplex = "half";74 }7576 let upstream: Response;77 try {78 upstream = await fetch(url, init);79 } catch (e) {80 const timeout = e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError");81 return json(timeout ? 504 : 502, { error: timeout ? "upstream timeout" : `upstream unreachable: ${e instanceof Error ? e.message : "error"}`, statusCode: timeout ? 504 : 502 });82 }8384 const out = new Headers();85 for (const h of PASS_RESPONSE_HEADERS) {86 const v = upstream.headers.get(h);87 if (v) out.set(h, v);88 }89 out.set("cache-control", "no-store");90 out.set("x-robots-tag", "noindex, nofollow");91 out.set("x-content-type-options", "nosniff");92 // never let an HTML body render in the top frame from this origin93 if ((out.get("content-type") ?? "").includes("text/html")) out.set("content-security-policy", "sandbox; default-src 'none'");94 return new Response(method === "HEAD" ? null : upstream.body, { status: upstream.status, headers: out });95}9697export { proxy as GET, proxy as POST, proxy as PATCH, proxy as PUT, proxy as DELETE, proxy as HEAD };98